Skip to content

fix(docker): pre-pull the Dockerfile syntax frontend image - #150

Merged
bilby91 merged 2 commits into
mainfrom
fix/build-syntax-frontend-prepull
Nov 4, 2026
Merged

bilby91 merged 2 commits into
mainfrom
fix/build-syntax-frontend-prepull

Conversation

@bilby91

@bilby91 bilby91 commented Nov 4, 2026

Copy link
Copy Markdown
Member

Fixes #148.

Cause

BuildImage builds through ImageBuild without a BuildKit session. BuildKit releases before v0.31 (Docker Engine before 29.6) refuse to contact the registry without a session, even for anonymous public pulls. Docker Hub answers with a bearer challenge, and without a session that fails as "no active sessions". The anonymous fallback arrived in moby/buildkit eb866c3d2.

FROM images were already pre-pulled through the classic pull API for exactly this reason. A # syntax= frontend was not, so on older engines any user Dockerfile naming one failed before parsing.

Engine Bundled BuildKit Session-less public pull
≤ 29.5.x ≤ v0.30.0 fails
≥ 29.6.0 ≥ v0.31.0 works

Fix

The issue's option 1: the existing pre-pull now also covers the frontend image.

  • The frontend comes from the BUILDKIT_SYNTAX build arg if set, otherwise from the syntax parser directive.
  • The directive is read the way BuildKit's parser.DetectSyntax reads it:
    • leading directive lines only;
    • # or // comments at column 0;
    • after an optional BOM or #! line;
    • the image is the value's first word.

Options not taken:

  • Option 2 (dropping the directive): it rewrites the user's file and doesn't cover custom frontends.
  • Option 3 (a real session): still worth doing, but for a different reason. The pre-pull passes no registry credentials, so private base images and frontends fail without a session on every engine version. That deserves its own issue.

Testing

  • Unit: TestSyntaxFrontend has 15 cases, one per parser rule.
  • Integration: TestEngineBuild_BuildSource_SyntaxDirective uses # syntax=docker/dockerfile:1 with COPY --chmod and RUN --mount.
    • Against a Docker 29.5.0 daemon (containerd image store, like the reporter's) it fails with the reported error without this change and passes with it.
    • On CI's Docker 29.4.2 it guards the fix. On 29.6+ engines it passes either way.
  • go vet, lint and go test -race ./... pass.

🤖 Generated with Claude Code

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — 2a1be4c · ADVISORY

The pre-pull still misses a BuildKit-supported JSON syntax declaration, so a custom frontend in that form remains subject to the older session-less resolution failure.

Finding Anchor
🟡 MEDIUM JSON syntax directives are not pre-pulled
core/correctness
build.go:402
What I reviewed

Coverage: Reviewed commit 2a1be4c14b13cc54ef9d21b52ba96260a27d9a4a (one commit; four changed files). Worked correctness, interface-contract, test-coverage, redelivery (not applicable), and version-compatibility. Traced BuildImage into BuildKit v0.30.0's actual Dockerfile parser and frontend dispatch; checked the new detector against that parser and its parser tests. Also read the repository's engineering review guidance at the governing base SHA: .dap/code-review.yml is absent, so the repository-specific R1–R8 guidance in .dap/review/engineering.md was considered alongside the core dimensions. Build context/cache determinism, lifecycle, path parity, and config flow are unaffected. The integration test covers the comment directive form but not BuildKit's supported JSON syntax form. No further unresolved candidates.


ADVISORY — findings worth reading, none of them blocking. Reply in a finding's own thread to argue it. On the pull request I answer @dap-code-review-by-crunchloop review, pause and resume.

Comment thread runtime/docker/build.go Resolved
BuildImage builds through ImageBuild with no BuildKit session, and
BuildKit before v0.31 (Docker Engine before 29.6) refuses registry
access without one, even for anonymous public pulls: a bearer
challenge with no session fails with "no active sessions". FROM
images were already pre-pulled through the classic pull API for this
reason; a `# syntax=` frontend was not, so any user Dockerfile naming
one failed before parsing (#148).

Pre-pull the frontend too: the BUILDKIT_SYNTAX build arg, else the
syntax parser directive, read as BuildKit's parser.DetectSyntax reads
it (leading directive lines only, # or // comments, after an optional
BOM and shebang; the image is the value's first word).

Verified against a Docker 29.5.0 daemon (containerd image store):
the new integration test fails there with the reported error without
this change and passes with it. Docker 29.6+ resolves anonymously
without a session, so it passes there either way.

Fixes #148.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bilby91
bilby91 force-pushed the fix/build-syntax-frontend-prepull branch from 2a1be4c to 7a81075 Compare November 4, 2026 17:39

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — 7a81075 · ADVISORY

Answer: One existing correctness finding remains open: pre-pull detection still omits a syntax form accepted by BuildKit. I found no additional independent defects in this change.

Finding Anchor
🟡 MEDIUM JSON syntax directives are not pre-pulled
core/correctness
build.go:402
What I reviewed

Coverage: Reviewed the single commit at 7a81075b7879e7a999c25321e5266a98559ae819 (four files) for correctness, interface contract, path parity, test coverage, and operability. Verified the frontend-selection behavior against BuildKit v0.30.0 source and traced pre-pulling through the Docker runtime build path. The repository’s governing standard is .dap/review/engineering.md at the directives base SHA; .dap/code-review.yml and AGENTS.md are absent. The added unit cases cover directive parsing and argument precedence, and the integration test covers a parser-directive frontend; JSON syntax input is not covered. No schema or published interface changed, so version compatibility does not apply. No other unreviewed changed behavior remains.


ADVISORY — findings worth reading, none of them blocking. Reply in a finding's own thread to argue it. On the pull request I answer @dap-code-review-by-crunchloop review, pause and resume.

BuildKit's parser.DetectSyntax falls back, after the # and // directive
forms, to a JSON object with a string "syntax" member, the input of
custom frontends that take JSON, and uses the whole value as the
frontend image. syntaxFrontend left that form out, so its frontend was
not pre-pulled and hit the session-less resolution failure on engines
before 29.6 (DAP review finding 3891 on #150).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — 8c50252 · CLEAN

Result. At HEAD 8c50252782a02a0ee10be1116876ca0bbb91ea30, the carried correctness finding is fixed: the JSON syntax fallback now detects and pre-pulls the frontend BuildKit selects. I found no additional concrete defects in the new change.

What I reviewed

Coverage. Reviewed the new head commit (2 changed files) and sanity-checked the branch range (3 commits, 17 files); this was a re-review of the new commit, so the earlier lockfile changes were not re-investigated. Checked correctness, the parser behavior contract, and test coverage: compared the implementation with BuildKit v0.30.0 DetectSyntax, followed the call through prePullBaseImages to ImageBuild, and verified unit cases for JSON syntax and precedence. Repository directives were read from the governing SHA: .dap/review/engineering.md; .dap/code-review.yml and AGENTS.md are absent there. No backend behavior beyond the Docker backend was reached; other conditional dimensions do not apply to this parser addition.


CLEAN — this round found nothing. Reply in a finding's own thread to argue it. On the pull request I answer @dap-code-review-by-crunchloop review, pause and resume.

@dap-code-review-by-crunchloop dap-code-review-by-crunchloop Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No open findings on 8c50252.

The DAP Code Review check run remains the verdict of record. This approval is dismissed if a finding reopens or a later head is not clean.

@bilby91
bilby91 merged commit fd46202 into main Nov 4, 2026
10 checks passed
@bilby91
bilby91 deleted the fix/build-syntax-frontend-prepull branch November 4, 2026 20:11
bilby91 added a commit that referenced this pull request Nov 5, 2026
* fix(docker): pre-pull the Dockerfile syntax frontend image (#150)

* fix(docker): pre-pull the Dockerfile syntax frontend image

BuildImage builds through ImageBuild with no BuildKit session, and
BuildKit before v0.31 (Docker Engine before 29.6) refuses registry
access without one, even for anonymous public pulls: a bearer
challenge with no session fails with "no active sessions". FROM
images were already pre-pulled through the classic pull API for this
reason; a `# syntax=` frontend was not, so any user Dockerfile naming
one failed before parsing (#148).

Pre-pull the frontend too: the BUILDKIT_SYNTAX build arg, else the
syntax parser directive, read as BuildKit's parser.DetectSyntax reads
it (leading directive lines only, # or // comments, after an optional
BOM and shebang; the image is the value's first word).

Verified against a Docker 29.5.0 daemon (containerd image store):
the new integration test fails there with the reported error without
this change and passes with it. Docker 29.6+ resolves anonymously
without a session, so it passes there either way.

Fixes #148.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(docker): pre-pull a frontend named by a JSON syntax document

BuildKit's parser.DetectSyntax falls back, after the # and // directive
forms, to a JSON object with a string "syntax" member, the input of
custom frontends that take JSON, and uses the whole value as the
frontend image. syntaxFrontend left that form out, so its frontend was
not pre-pulled and hit the session-less resolution failure on engines
before 29.6 (DAP review finding 3891 on #150).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: bilby91 <2201079+bilby91@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(features): enforce lockfile pins on baked features and empty configs (#153)

* fix(features): enforce lockfile pins on baked features and empty configs

Two findings from the review of #146:

- A feature the base image's label satisfied was skipped and its
  existing lock entry copied, so frozen mode accepted a base image
  built from another artifact, or one the lockfile had no entry for.
  The label already records each feature's resolvedRef, but
  ParseMetadataLabel dropped it (and so did carrying base entries into
  a rebuilt label). Keep it on FeatureMetadata, read the lockfile
  before matching the label, and accept a baked feature only when its
  artifact is the pinned one or, with no pin, a recorded one, which is
  then locked. Anything else is fetched at its pin and installed.
  LockfileDisabled keeps the label-only match.
- layerFeatures returned before the lockfile when no features were
  declared, so removing the last feature left its entry behind and
  frozen mode accepted it. Reconcile an existing lockfile then too:
  default and upgrade empty it, frozen reports the stale entries.
  Without a lockfile nothing is created, as with the reference CLI.

The pre-baked hot path is unchanged for images this library built,
whose labels have recorded resolvedRef since v0.1.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(features): re-resolve baked features in lockfile upgrade mode

acceptsBaked found no pin in LockfileUpgrade (pin ignores the lockfile
there) and so accepted any recorded baked artifact, which commit then
locked: an upgrade could replace a newer pin with the older artifact
the base image carries (DAP review finding 3902 on #153). Upgrade mode
now accepts no baked feature, so each reference is resolved afresh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: bilby91 <2201079+bilby91@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(features): install features reachable only through dependsOn

dependsOn only ordered features that devcontainer.json declared anyway:
an undeclared dependency was silently skipped, so a feature relying on
one installed without it (#147). The spec makes dependsOn a hard
dependency, installed recursively with the options it names.

- layerFeatures' fetch loop appends each fetched feature's dependsOn
  requirements and fetches them in turn: lockfile pinning, option
  validation and defaults apply as for declared features, and they are
  locked under their dependsOn key as the reference CLI does.
- Spec equality decides duplicates: same content (digest) and same
  options once defaults apply. A repeat of a key with the same options
  is skipped before fetching, which also ends cycles; the same content
  under another reference is dropped after. One feature with different
  options is installed once per set, and topoSort maps an id to all
  its entries, pointing dependsOn edges at the one with the requested
  options.
- A dependency the base image's label satisfies is not fetched or
  installed again: bakedFeature, factored from markAlreadyInstalled,
  includes the lockfile's check of the artifact the label records.
  Its own dependencies are still followed, as are a declared baked
  feature's, since an image built before this change lacks them.
  Labels now record each feature's dependsOn ({} when none); for a
  label written before that, the baked feature's metadata is fetched
  at its recorded artifact to find them.
- A dependency dropped as a duplicate under another reference is still
  locked under its own key.
- The compose inputs hash also covers a local dependency's content,
  which the declaring feature's content does not; projects without one
  hash as before.
- OCI references are lowercased before parsing, as the CLI's getRef
  does: published dependsOn keys such as ghcr.io/codspace/dependson/A
  failed to parse.

The integration test builds the CLI's own lockfile-dependson fixture in
frozen mode against its expected lockfile.

Closes #147.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(features): record the baked match's option gap for dependencies

The DAP review of #151 (finding 3903) notes a baked dependency is reused
whatever options it was requested with. The pre-baked match has never
compared options, for declared features either; record the limitation
in §5.1 and track the fix to the match itself in #154.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(features): lock an unpinned baked feature with its dependsOn

The DAP review of #153 (finding 3904) noted a baked feature accepted
without a pin was locked from its label entry, which then had no
dependsOn. This change fills it in before commit; pin it with a test
for a label that records dependsOn and for an older one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(features): confine local dependsOn paths

The DAP review of #151 (findings 3912, 3919, 3917) showed a fetched
feature's dependsOn could name a local path, absolute or escaping with
.., which layerFeatures resolved against devcontainer.json's directory,
copied into the build context and ran: published metadata choosing a
host directory to execute. The compose inputs hash also missed such a
dependency, since it follows local dependencies from declared local
features only.

A local dependsOn key is now followed only from a local feature, and
only as a relative path that resolves, symlinks included, inside the
directory of devcontainer.json, as the reference CLI requires of local
features; anything else fails Up and Build. With remote features unable
to reach local dependencies, the compose hash's traversal covers every
one. Declared local features keep their existing resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(features): never take a dependsOn dependency from the base image

The DAP review of #151 (finding 3913) showed a dependency the base
image's label listed was reused whatever options its dependsOn entry
asked for: the pre-baked match compares id and version only, and those
options are the feature author's, which no user config could route
through that match before. Dependencies are now always fetched and
installed; a declared baked feature still is not reinstalled, and its
dependencies are installed. Until #154 adds option matching, a
pre-baked image whose features have dependencies reinstalls them on
every build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: bilby91 <2201079+bilby91@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Builds fail with 'no active sessions' when a Dockerfile declares '# syntax=docker/dockerfile:1'

1 participant